<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-metadata</title>

<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="../../core/gr-router/gr-router.html">
<link rel="import" href="gr-change-metadata.html">

<script>void(0);</script>

<test-fixture id="basic">
  <template>
    <gr-change-metadata></gr-change-metadata>
  </template>
</test-fixture>

<script>
  suite('gr-change-metadata tests', () => {
    let element;
    let sandbox;

    setup(() => {
      sandbox = sinon.sandbox.create();
      stub('gr-endpoint-decorator', {
        _import: sandbox.stub().returns(Promise.resolve()),
      });
      stub('gr-rest-api-interface', {
        getConfig() { return Promise.resolve({}); },
        getLoggedIn() { return Promise.resolve(false); },
      });

      element = fixture('basic');
    });

    teardown(() => {
      sandbox.restore();
    });

    test('computed fields', () => {
      assert.isFalse(element._computeHideStrategy({status: 'NEW'}));
      assert.isTrue(element._computeHideStrategy({status: 'MERGED'}));
      assert.isTrue(element._computeHideStrategy({status: 'ABANDONED'}));
      assert.equal(element._computeStrategy({submit_type: 'CHERRY_PICK'}),
          'Cherry Pick');
      assert.equal(element._computeStrategy({submit_type: 'REBASE_ALWAYS'}),
          'Rebase Always');
    });

    test('show strategy for open change', () => {
      element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}};
      flushAsynchronousOperations();
      const strategy = element.$$('.strategy');
      assert.ok(strategy);
      assert.isFalse(strategy.hasAttribute('hidden'));
      assert.equal(strategy.children[1].innerHTML, 'Cherry Pick');
    });

    test('hide strategy for closed change', () => {
      element.change = {status: 'MERGED', labels: {}};
      flushAsynchronousOperations();
      assert.isTrue(element.$$('.strategy').hasAttribute('hidden'));
    });

    test('show CC section when NoteDb enabled', () => {
      function hasCc() {
        return element._showReviewersByState;
      }

      element.serverConfig = {};
      assert.isFalse(hasCc());

      element.serverConfig = {note_db_enabled: true};
      assert.isTrue(hasCc());
    });

    test('computes submit status', () => {
      let showMissingLabels = false;
      sandbox.stub(element, '_showMissingLabels', () => {
        return showMissingLabels;
      });
      assert.isFalse(element._showMissingRequirements(null, false));
      assert.isTrue(element._showMissingRequirements(null, true));
      showMissingLabels = true;
      assert.isTrue(element._showMissingRequirements(null, false));
    });

    test('show missing labels', () => {
      let missingLabels = [];
      assert.isFalse(element._showMissingLabels(missingLabels));
      missingLabels = ['test'];
      assert.isTrue(element._showMissingLabels(missingLabels));
      missingLabels.push('test2');
      assert.isTrue(element._showMissingLabels(missingLabels));
    });

    test('weblinks use Gerrit.Nav interface', () => {
      const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
          .returns([{name: 'stubb', url: '#s'}]);
      element.commitInfo = {};
      flushAsynchronousOperations();
      const webLinks = element.$.webLinks;
      assert.isTrue(weblinksStub.called);
      assert.isFalse(webLinks.hasAttribute('hidden'));
      assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
    });

    test('weblinks hidden when no weblinks', () => {
      element.commitInfo = {};
      flushAsynchronousOperations();
      const webLinks = element.$.webLinks;
      assert.isTrue(webLinks.hasAttribute('hidden'));
    });

    test('weblinks hidden when only gitiles weblink', () => {
      element.commitInfo = {web_links: [{name: 'gitiles', url: '#'}]};
      flushAsynchronousOperations();
      const webLinks = element.$.webLinks;
      assert.isTrue(webLinks.hasAttribute('hidden'));
      assert.equal(element._computeWebLinks(element.commitInfo), null);
    });

    test('weblinks are visible when other weblinks', () => {
      const router = document.createElement('gr-router');
      sandbox.stub(Gerrit.Nav, '_generateWeblinks',
          router._generateWeblinks.bind(router));

      element.commitInfo = {web_links: [{name: 'test', url: '#'}]};
      flushAsynchronousOperations();
      const webLinks = element.$.webLinks;
      assert.isFalse(webLinks.hasAttribute('hidden'));
      assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
      // With two non-gitiles weblinks, there are two returned.
      element.commitInfo = {
        web_links: [{name: 'test', url: '#'}, {name: 'test2', url: '#'}]};
      assert.equal(element._computeWebLinks(element.commitInfo).length, 2);
    });

    test('weblinks are visible when gitiles and other weblinks', () => {
      const router = document.createElement('gr-router');
      sandbox.stub(Gerrit.Nav, '_generateWeblinks',
          router._generateWeblinks.bind(router));

      element.commitInfo = {
        web_links: [{name: 'test', url: '#'}, {name: 'gitiles', url: '#'}]};
      flushAsynchronousOperations();
      const webLinks = element.$.webLinks;
      assert.isFalse(webLinks.hasAttribute('hidden'));
      // Only the non-gitiles weblink is returned.
      assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
    });

    test('determines whether to show "Ready to Submit" label', () => {
      const showMissingSpy = sandbox.spy(element, '_showMissingRequirements');
      element.missingLabels = ['bojack'];
      element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {
        test: {
          all: [{_account_id: 1, name: 'bojack', value: 1}],
          default_value: 0,
          values: [],
        },
      }};
      flushAsynchronousOperations();
      assert.isTrue(showMissingSpy.called);
    });

    test('_computeShowUploader test for uploader', () => {
      const change = {
        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
        owner: {
          _account_id: 1019328,
        },
        revisions: {
          rev1: {
            _number: 1,
            uploader: {
              _account_id: 1011123,
            },
          },
        },
        current_revision: 'rev1',
        status: 'NEW',
        labels: {},
        mergeable: true,
      };
      assert.deepEqual(element._computeShowUploader(change),
          {_account_id: 1011123});
    });

    test('_computeShowUploader test that it does not return uploader', () => {
      const change = {
        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
        owner: {
          _account_id: 1011123,
        },
        revisions: {
          rev1: {
            _number: 1,
            uploader: {
              _account_id: 1011123,
            },
          },
        },
        current_revision: 'rev1',
        status: 'NEW',
        labels: {},
        mergeable: true,
      };
      assert.isNotOk(element._computeShowUploader(change));
    });

    test('no current_revision makes _computeShowUploader return null', () => {
      const change = {
        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
        owner: {
          _account_id: 1011123,
        },
        revisions: {
          rev1: {
            _number: 1,
            uploader: {
              _account_id: 1011123,
            },
          },
        },
        status: 'NEW',
        labels: {},
        mergeable: true,
      };
      assert.isNotOk(element._computeShowUploader(change));
    });

    test('_computeShowUploaderHide test for string which equals true', () => {
      const change = {
        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
        owner: {
          _account_id: 1019328,
        },
        revisions: {
          rev1: {
            _number: 1,
            uploader: {
              _account_id: 1011123,
            },
          },
        },
        current_revision: 'rev1',
        status: 'NEW',
        labels: {},
        mergeable: true,
      };
      assert.equal(element._computeShowUploaderHide(change), '');
    });

    test('_computeShowUploaderHide test for hideDisplay', () => {
      const change = {
        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
        owner: {
          _account_id: 1011123,
        },
        revisions: {
          rev1: {
            _number: 1,
            uploader: {
              _account_id: 1011123,
            },
          },
        },
        current_revision: 'rev1',
        status: 'NEW',
        labels: {},
        mergeable: true,
      };
      assert.equal(
          element._computeShowUploaderHide(change), 'hideDisplay');
    });

    test('_computeValueTooltip', () => {
      // Existing label.
      const change = {labels: {'Foo-bar': {values: {0: 'Baz'}}}};
      let score = '0';
      let labelName = 'Foo-bar';
      let actual = element._computeValueTooltip(change, score, labelName);
      assert.equal(actual, 'Baz');

      // Non-extsistent label.
      labelName = 'xyz';
      actual = element._computeValueTooltip(change, score, labelName);
      assert.equal(actual, '');

      // Non-extsistent score.
      score = '2';
      actual = element._computeValueTooltip(change, score, labelName);
      assert.equal(actual, '');

      // No values on label.
      labelName = 'abcd';
      score = '0';
      change.labels.abcd = {};
      actual = element._computeValueTooltip(change, score, labelName);
      assert.equal(actual, '');
    });

    test('_computeParents', () => {
      const parents = [{commit: '123', subject: 'abc'}];
      assert.isUndefined(element._computeParents(
          {revisions: {456: {commit: {parents}}}}));
      assert.isUndefined(element._computeParents(
          {current_revision: '789', revisions: {456: {commit: {parents}}}}));
      assert.equal(element._computeParents(
          {current_revision: '456', revisions: {456: {commit: {parents}}}}),
          parents);
    });

    test('_computeParentsLabel', () => {
      const parent = {commit: 'abc123', subject: 'My parent commit'};
      assert.equal(element._computeParentsLabel([parent]), 'Parent');
      assert.equal(element._computeParentsLabel([parent, parent]),
          'Parents');
    });

    test('_computeParentListClass', () => {
      const parent = {commit: 'abc123', subject: 'My parent commit'};
      assert.equal(element._computeParentListClass([parent], true),
          'parentList nonMerge current');
      assert.equal(element._computeParentListClass([parent], false),
          'parentList nonMerge notCurrent');
      assert.equal(element._computeParentListClass([parent, parent], false),
          'parentList merge notCurrent');
      assert.equal(element._computeParentListClass([parent, parent], true),
          'parentList merge current');
    });

    test('_showAddTopic', () => {
      assert.isTrue(element._showAddTopic(null, false));
      assert.isTrue(element._showAddTopic({base: {topic: null}}, false));
      assert.isFalse(element._showAddTopic({base: {topic: null}}, true));
      assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, true));
      assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, false));
    });

    test('_showTopicChip', () => {
      assert.isFalse(element._showTopicChip(null, false));
      assert.isFalse(element._showTopicChip({base: {topic: null}}, false));
      assert.isFalse(element._showTopicChip({base: {topic: null}}, true));
      assert.isFalse(element._showTopicChip({base: {topic: 'foo'}}, true));
      assert.isTrue(element._showTopicChip({base: {topic: 'foo'}}, false));
    });

    suite('Topic removal', () => {
      let change;
      setup(() => {
        change = {
          _number: 'the number',
          actions: {
            topic: {enabled: false},
          },
          change_id: 'the id',
          topic: 'the topic',
          status: 'NEW',
          submit_type: 'CHERRY_PICK',
          labels: {
            test: {
              all: [{_account_id: 1, name: 'bojack', value: 1}],
              default_value: 0,
              values: [],
            },
          },
          removable_reviewers: [],
        };
      });

      test('_computeTopicReadOnly', () => {
        let mutable = false;
        assert.isTrue(element._computeTopicReadOnly(mutable, change));
        mutable = true;
        assert.isTrue(element._computeTopicReadOnly(mutable, change));
        change.actions.topic.enabled = true;
        assert.isFalse(element._computeTopicReadOnly(mutable, change));
        mutable = false;
        assert.isTrue(element._computeTopicReadOnly(mutable, change));
      });

      test('topic read only hides delete button', () => {
        element.mutable = false;
        element.change = change;
        flushAsynchronousOperations();
        const button = element.$$('gr-linked-chip').$$('gr-button');
        assert.isTrue(button.hasAttribute('hidden'));
      });

      test('topic not read only does not hide delete button', () => {
        element.mutable = true;
        change.actions.topic.enabled = true;
        element.change = change;
        flushAsynchronousOperations();
        const button = element.$$('gr-linked-chip').$$('gr-button');
        assert.isFalse(button.hasAttribute('hidden'));
      });
    });

    suite('Hashtag removal', () => {
      let change;
      setup(() => {
        change = {
          _number: 'the number',
          actions: {
            hashtags: {enabled: false},
          },
          change_id: 'the id',
          hashtags: ['test-hashtag'],
          status: 'NEW',
          submit_type: 'CHERRY_PICK',
          labels: {
            test: {
              all: [{_account_id: 1, name: 'bojack', value: 1}],
              default_value: 0,
              values: [],
            },
          },
          removable_reviewers: [],
        };
      });

      test('_computeHashtagReadOnly', () => {
        element.serverConfig = {
          note_db_enabled: true,
        };
        flushAsynchronousOperations();
        let mutable = false;
        assert.isTrue(element._computeHashtagReadOnly(mutable, change));
        mutable = true;
        assert.isTrue(element._computeHashtagReadOnly(mutable, change));
        change.actions.hashtags.enabled = true;
        assert.isFalse(element._computeHashtagReadOnly(mutable, change));
        mutable = false;
        assert.isTrue(element._computeHashtagReadOnly(mutable, change));
      });

      test('hashtag read only hides delete button', () => {
        element.serverConfig = {
          note_db_enabled: true,
        };
        flushAsynchronousOperations();
        element.mutable = false;
        element.change = change;
        flushAsynchronousOperations();
        const button = element.$$('gr-linked-chip').$$('gr-button');
        assert.isTrue(button.hasAttribute('hidden'));
      });

      test('hashtag not read only does not hide delete button', () => {
        element.serverConfig = {
          note_db_enabled: true,
        };
        flushAsynchronousOperations();
        element.mutable = true;
        change.actions.hashtags.enabled = true;
        element.change = change;
        flushAsynchronousOperations();
        const button = element.$$('gr-linked-chip').$$('gr-button');
        assert.isFalse(button.hasAttribute('hidden'));
      });
    });

    suite('remove reviewer votes', () => {
      setup(() => {
        sandbox.stub(element, '_computeValueTooltip').returns('');
        sandbox.stub(element, '_computeTopicReadOnly').returns(true);
        element.change = {
          _number: 42,
          change_id: 'the id',
          actions: [],
          topic: 'the topic',
          status: 'NEW',
          submit_type: 'CHERRY_PICK',
          labels: {
            test: {
              all: [{_account_id: 1, name: 'bojack', value: 1}],
              default_value: 0,
              values: [],
            },
          },
          removable_reviewers: [],
        };
        flushAsynchronousOperations();
      });

      test('_computeCanDeleteVote hides delete button', () => {
        const button = element.$$('gr-account-chip').$$('gr-button');
        assert.isTrue(button.hasAttribute('hidden'));
        element.mutable = true;
        assert.isTrue(button.hasAttribute('hidden'));
      });

      test('_computeCanDeleteVote shows delete button', () => {
        element.change.removable_reviewers = [
          {
            _account_id: 1,
            name: 'bojack',
          },
        ];
        element.mutable = true;
        const button = element.$$('gr-account-chip').$$('gr-button');
        assert.isFalse(button.hasAttribute('hidden'));
      });

      test('deletes votes', done => {
        const deleteStub = sandbox.stub(element.$.restAPI, 'deleteVote')
            .returns(Promise.resolve({ok: true}));

        element.change.removable_reviewers = [
          {
            _account_id: 1,
            name: 'bojack',
          },
        ];
        element.change.labels.test.recommended = {_account_id: 1};
        element.mutable = true;
        flushAsynchronousOperations();
        const chip = element.$$('gr-account-chip');
        const button = chip.$$('gr-button');

        const spliceStub = sandbox.stub(element, 'splice', (path, index,
            length) => {
          assert.isFalse(chip.disabled);
          assert.deepEqual(path, ['change.labels', 'test', 'all']);
          assert.equal(index, 0);
          assert.equal(length, 1);
          assert.notOk(element.change.labels.test.recommended);
          assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test'));
          spliceStub.restore();
          done();
        });

        MockInteractions.tap(button);
        assert.isTrue(chip.disabled);
      });

      test('changing topic', () => {
        const newTopic = 'the new topic';
        sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
            Promise.resolve(newTopic));
        element._handleTopicChanged({}, newTopic);
        const topicChangedSpy = sandbox.spy();
        element.addEventListener('topic-changed', topicChangedSpy);
        assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
            42, newTopic));
        return element.$.restAPI.setChangeTopic.lastCall.returnValue
            .then(() => {
              assert.equal(element.change.topic, newTopic);
              assert.isTrue(topicChangedSpy.called);
            });
      });

      test('topic removal', () => {
        sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
            Promise.resolve());
        const chip = element.$$('gr-linked-chip');
        const remove = chip.$.remove;
        const topicChangedSpy = sandbox.spy();
        element.addEventListener('topic-changed', topicChangedSpy);
        MockInteractions.tap(remove);
        assert.isTrue(chip.disabled);
        assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
            42, null));
        return element.$.restAPI.setChangeTopic.lastCall.returnValue
            .then(() => {
              assert.isFalse(chip.disabled);
              assert.equal(element.change.topic, '');
              assert.isTrue(topicChangedSpy.called);
            });
      });

      test('changing hashtag', () => {
        element.serverConfig = {
          note_db_enabled: true,
        };
        flushAsynchronousOperations();
        element._newHashtag = 'new hashtag';
        const newHashtag = ['new hashtag'];
        sandbox.stub(element.$.restAPI, 'setChangeHashtag').returns(
            Promise.resolve(newHashtag));
        element._handleHashtagChanged({}, 'new hashtag');
        assert.isTrue(element.$.restAPI.setChangeHashtag.calledWith(
            42, {add: ['new hashtag']}));
        return element.$.restAPI.setChangeHashtag.lastCall.returnValue
            .then(() => {
              assert.equal(element.change.hashtags, newHashtag);
            });
      });

      suite('assignee field', () => {
        const dummyAccount = {
          _account_id: 1,
          name: 'bojack',
        };
        const change = {
          actions: {
            assignee: {enabled: false},
          },
          assignee: dummyAccount,
        };
        let deleteStub;
        let setStub;

        setup(() => {
          deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee');
          setStub = sandbox.stub(element.$.restAPI, 'setAssignee');
        });

        test('changing change recomputes _assignee', () => {
          assert.isFalse(!!element._assignee.length);
          const change = element.change;
          change.assignee = dummyAccount;
          element._changeChanged(change);
          assert.deepEqual(element._assignee[0], dummyAccount);
        });

        test('modifying _assignee calls API', () => {
          assert.isFalse(!!element._assignee.length);
          element.set('_assignee', [dummyAccount]);
          assert.isTrue(setStub.calledOnce);
          assert.deepEqual(element.change.assignee, dummyAccount);
          element.set('_assignee', [dummyAccount]);
          assert.isTrue(setStub.calledOnce);
          element.set('_assignee', []);
          assert.isTrue(deleteStub.calledOnce);
          assert.equal(element.change.assignee, undefined);
          element.set('_assignee', []);
          assert.isTrue(deleteStub.calledOnce);
        });

        test('_computeAssigneeReadOnly', () => {
          let mutable = false;
          assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
          mutable = true;
          assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
          change.actions.assignee.enabled = true;
          assert.isFalse(element._computeAssigneeReadOnly(mutable, change));
          mutable = false;
          assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
        });
      });
    });

    suite('plugin endpoints', () => {
      test('endpoint params', done => {
        element.change = {labels: {}};
        element.revision = {};
        let hookEl;
        let plugin;
        Gerrit.install(
            p => {
              plugin = p;
              plugin.hook('change-metadata-item').getLastAttached().then(
                  el => hookEl = el);
            },
            '0.1',
            'http://some/plugins/url.html');
        Gerrit._setPluginsCount(0);
        flush(() => {
          assert.strictEqual(hookEl.plugin, plugin);
          assert.strictEqual(hookEl.change, element.change);
          assert.strictEqual(hookEl.revision, element.revision);
          done();
        });
      });
    });

    suite('label colors', () => {
      test('valueless label rejected', () => {
        element.change = {
          labels: {
            'Do-Not-Submit': {
              rejected: {name: 'someone'},
            },
          },
        };
        flushAsynchronousOperations();
        const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
        assert.isTrue(labels[0].classList.contains('negative'));
      });

      test('valueless label approved', () => {
        element.change = {
          labels: {
            'To-The-Infinity': {
              approved: {name: 'someone'},
            },
          },
        };
        flushAsynchronousOperations();
        const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
        assert.isTrue(labels[0].classList.contains('positive'));
      });

      test('-2 to +2', () => {
        element.change = {
          labels: {
            'Code-Review': {
              all: [
                {value: 2, name: 'user 2'},
                {value: 1, name: 'user 1'},
                {value: -1, name: 'user 3'},
                {value: -2, name: 'user 4'},
              ],
              values: {
                '-2': 'Awful',
                '-1': 'Don\'t submit as-is',
                ' 0': 'No score',
                '+1': 'Looks good to me',
                '+2': 'Ready to submit',
              },
            },
          },
        };
        flushAsynchronousOperations();
        const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
        assert.isTrue(labels[0].classList.contains('max'));
        assert.isTrue(labels[1].classList.contains('positive'));
        assert.isTrue(labels[2].classList.contains('negative'));
        assert.isTrue(labels[3].classList.contains('min'));
      });

      test('-1 to +1', () => {
        element.change = {
          labels: {
            CI: {
              all: [
                {value: 1, name: 'user 1'},
                {value: -1, name: 'user 2'},
              ],
              values: {
                '-1': 'Don\'t submit as-is',
                ' 0': 'No score',
                '+1': 'Looks good to me',
              },
            },
          },
        };
        flushAsynchronousOperations();
        const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
        assert.isTrue(labels[0].classList.contains('max'));
        assert.isTrue(labels[1].classList.contains('min'));
      });

      test('0 to +2', () => {
        element.change = {
          labels: {
            CI: {
              all: [
                {value: 1, name: 'user 2'},
                {value: 2, name: 'user '},
              ],
              values: {
                ' 0': 'Don\'t submit as-is',
                '+1': 'No score',
                '+2': 'Looks good to me',
              },
            },
          },
        };
        flushAsynchronousOperations();
        const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
        assert.isTrue(labels[0].classList.contains('positive'));
        assert.isTrue(labels[1].classList.contains('max'));
      });
    });
  });
</script>
